/*
 * Copyright 2011 myTDev.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mytdev.commons.io;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * A class to zip and unzip files.
 * 
 * @author Yann D'Isanto
 */
public class Zipper {

    /** Compression level for best compression. */
    public static final int BEST_COMPRESSION_LEVEL = Deflater.BEST_COMPRESSION;
    /** Default compression level. */
    public static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION;
    /** Compression level for no compression. */
    public static final int NO_COMPRESSION_LEVEL = Deflater.NO_COMPRESSION;
    private File workingDirectory;
    private int compressionLevel;

    
    /**
     * Creates a Zipper with the current working directory and the default compression level.
     */
    public Zipper() {
        this(new File(""), DEFAULT_COMPRESSION_LEVEL);
    }

    /**
     * Creates a Zipper with the default compression level and the specified working directory.
     * @param workingDirectory the working directory.
     */
    public Zipper(File workingDirectory) {
        this(workingDirectory, DEFAULT_COMPRESSION_LEVEL);
    }

    /**
     * Creates a Zipper with the specified working directory and compression level.
     * @param workingDirectory the working directory.
     * @param compressionLevel the compression level.
     */
    public Zipper(File workingDirectory, int compressionLevel) {
        this.workingDirectory = workingDirectory;
        this.compressionLevel = compressionLevel;
    }

    /**
     * Creates a Zipper with the current working directory and the specified compression level.
     * @param compressionLevel the compression level.
     */
    public Zipper(int compressionLevel) {
        this(new File(""), compressionLevel);
    }

    /**
     * Returns the compression level.
     * @return the compression level. 
     */
    public int getCompressionLevel() {
        return compressionLevel;
    }

    /**
     * Sets the compression level.
     * @param level the compression level.
     * @return this Zipper.
     */
    public Zipper setCompressionLevel(int level) {
        this.compressionLevel = level;
        return this;
    }

    /**
     * Returns the working directory.
     * @return the working directory. 
     */
    public File getWorkingDirectory() {
        return workingDirectory;
    }

    /**
     * Sets the working directory.
     * @param workingDirectory the working directory.
     * @return this Zipper.
     */
    public Zipper setWorkingDirectory(File workingDirectory) {
        this.workingDirectory = workingDirectory;
        return this;
    }

    /**
     * Zips the specified entries into the specified destination file. The 
     * entries are added.
     * @param destination the destination file.
     * @param entries the entries to zip.
     * @throws ZipException if a ZIP file error has occurred.
     * @throws IOException if an I/O exception has occurred.
     */
    public void zip(File dest, String[] entries) throws ZipException, IOException {
        zip(dest, entries, true);
    }
    
    /**
     * Zips the specified entries into the specified destination file. The 
     * entries are added if the specified append flag is true.
     * @param destination the destination file.
     * @param entries the entries to zip.
     * @param append the append flag.
     * @throws ZipException if a ZIP file error has occurred.
     * @throws IOException if an I/O exception has occurred.
     */
    public void zip(File destination, String[] entries, boolean append) throws ZipException, IOException {
        FileOutputStream fos = new FileOutputStream(destination, append);
        try {
            ZipOutputStream zos = new ZipOutputStream(fos);
            zos.setMethod(ZipOutputStream.DEFLATED);
            zos.setLevel(compressionLevel);
            for (String entry : entries) {
                File file = new File(workingDirectory, entry);
                if (!file.isDirectory() && !file.isFile()) {
                    throw new IllegalArgumentException("invalid entry: " + entry);
                }
                ZipEntry zipEntry = new ZipEntry(entry);
                zipEntry.setMethod(file.isDirectory() ? ZipOutputStream.STORED : ZipOutputStream.DEFLATED);
                zos.putNextEntry(zipEntry);
                if (file.isFile()) {
                    FileInputStream fis = new FileInputStream(file);
                    try {
                        BufferedOutputStream output = new BufferedOutputStream(zos);
                        BufferedInputStream input = new BufferedInputStream(fis);
                        byte[] buffer = new byte[2048];
                        int nbRead = 0;
                        while ((nbRead = input.read(buffer)) > 0) {
                            output.write(buffer, 0, nbRead);
                            output.flush();
                        }
                    } finally {
                        fis.close();
                    }
                }
            }
            zos.finish();
        } finally {
            fos.close();
        }
    }

    /**
     * Unzips the specified file.
     * @param file the file to unzip.
     * @throws ZipException if a ZIP file error has occurred.
     * @throws IOException if an I/O exception has occurred.
     */
    public void unzip(File file) throws ZipException, IOException {
        unzip(file, null);
    }

    /**
     * Unzips the specified entries from the specified file.
     * @param file the file to unzip.
     * @throws ZipException if a ZIP file error has occurred.
     * @throws IOException if an I/O exception has occurred.
     */
    public void unzip(File file, String[] entries) throws ZipException, IOException {
        ZipFile zipFile = new ZipFile(file);
        try {
            Enumeration<? extends ZipEntry> entriesEnumeration = zipFile.entries();
            if (entries != null) {
                List<ZipEntry> entriesToUnzip = new ArrayList<ZipEntry>();
                for (String entryName : entries) {
                    ZipEntry entry = zipFile.getEntry(entryName);
                    if (entry == null) {
                        throw new IllegalArgumentException("zip entry not foud in " + file + ": " + entryName);
                    }
                    entriesToUnzip.add(entry);
                }
                entriesEnumeration = Collections.enumeration(entriesToUnzip);
            }
            while (entriesEnumeration.hasMoreElements()) {
                ZipEntry entry = entriesEnumeration.nextElement();
                File destination = new File(workingDirectory, entry.getName());
                if (entry.isDirectory()) {
                    destination.mkdirs();
                } else {
                    FileOutputStream fos = new FileOutputStream(destination);
                    try {
                        BufferedOutputStream output = new BufferedOutputStream(fos);
                        BufferedInputStream input = new BufferedInputStream(zipFile.getInputStream(entry));
                        byte[] buffer = new byte[2048];
                        int nbRead = 0;
                        while ((nbRead = input.read(buffer)) > 0) {
                            output.write(buffer, 0, nbRead);
                            output.flush();
                        }
                    } finally {
                        fos.close();
                    }
                }
            }
        } finally {
            zipFile.close();
        }
    }
}
